Skip to content

Add store support for Spin adapter (KV, config, secret)#253

Open
prk-Jr wants to merge 21 commits into
mainfrom
feat/spin-store-support
Open

Add store support for Spin adapter (KV, config, secret)#253
prk-Jr wants to merge 21 commits into
mainfrom
feat/spin-store-support

Conversation

@prk-Jr
Copy link
Copy Markdown
Contributor

@prk-Jr prk-Jr commented Apr 24, 2026

Summary

  • Implements SpinKvStore, SpinConfigStore, and SpinSecretStore so Spin handlers can access key-value, configuration, and secret data through the same ctx.kv_handle(), ctx.config_store(), and ctx.secret_handle() API as every other adapter.
  • Wires all three store handles into the Spin request dispatch pipeline so they are available on every request without any extra setup in user code.
  • Adds a spin-adapter-tests CI job, store injection contract tests, and Spin support to all three smoke test scripts.

Changes

Crate / File Change
crates/edgezero-adapter-spin/src/config_store.rs New — SpinConfigStore backed by spin_sdk::variables; dual-backend (Spin / in-memory for tests); contract tests via macro
crates/edgezero-adapter-spin/src/key_value_store.rs New — SpinKvStore backed by spin_sdk::key_value; in-process prefix filter + pagination; configurable max_list_keys cap (warn + truncate); TTL silently ignored
crates/edgezero-adapter-spin/src/secret_store.rs New — SpinSecretStore backed by spin_sdk::variables; key normalised to lowercase to match Spin variable naming rules
crates/edgezero-adapter-spin/src/request.rs dispatch() now injects ConfigStoreHandle, KvHandle, and SecretHandle into every request's extensions
crates/edgezero-adapter-spin/src/lib.rs Re-exports new store types
crates/edgezero-adapter-spin/tests/contract.rs Store injection smoke tests + wasm32 compile-time trait checks for SpinKvStore and SpinSecretStore
crates/edgezero-core/src/manifest.rs Added "spin" to SUPPORTED_CONFIG_STORE_ADAPTERS
crates/edgezero-core/src/config_store.rs Updated ConfigStore trait doc to list SpinConfigStore
.github/workflows/test.yml New spin-adapter-tests job: native tests + wasm32-wasip1 compilation check
examples/app-demo/crates/app-demo-adapter-spin/spin.toml Added key_value_stores = ["default"] binding and variable declarations for greeting and smoke_secret
examples/app-demo/edgezero.toml Added "spin" to adapters for config, KV, and secrets routes
scripts/smoke_test_kv.sh Added spin case
scripts/smoke_test_config.sh Added spin case; skips dotted-key checks (Spin variable names cannot contain dots)
scripts/smoke_test_secrets.sh Added spin case; passes secret value via SPIN_VARIABLE_SMOKE_SECRET at startup
.gitignore Added .spin/ (runtime SQLite KV database and component logs)

Closes

Closes #73
Closes #74

Test plan

  • cargo test --workspace --all-targets
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • WASM builds: cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --features spin
  • ./scripts/smoke_test_kv.sh spin
  • ./scripts/smoke_test_config.sh spin
  • ./scripts/smoke_test_secrets.sh spin

Checklist

  • Changes follow CLAUDE.md conventions
  • No Tokio deps added to core or adapter crates
  • Route params use {id} syntax (not :id)
  • Types imported from edgezero_core (not http crate)
  • New code has tests
  • No secrets or credentials committed

prk-Jr added 14 commits April 24, 2026 20:05
- dispatch() in request.rs now injects ConfigStoreHandle, KvHandle, and
  SecretHandle into request extensions on every request
- SpinSecretStore normalises the lookup key to lowercase so conventional
  uppercase names (e.g. SMOKE_SECRET) resolve to the correct Spin variable
- contract.rs gains store injection smoke tests (config, kv, secret) and
  wasm32 compile-time trait checks for SpinKvStore and SpinSecretStore
- spin.toml gains key_value_stores = ["default"] binding and variables
  declarations for greeting and smoke_secret
- edgezero.toml adds "spin" to adapters for config, kv, and secrets routes
- smoke_test_kv/config/secrets.sh each gain a spin case that builds the
  WASM binary and starts spin up --listen 127.0.0.1:3000; the config
  script skips dotted-key checks (Spin variable names cannot contain dots);
  the secrets script passes SPIN_VARIABLE_SMOKE_SECRET at startup
spin up creates .spin/ (SQLite KV database and component logs) in the
adapter directory during local development, mirroring .wrangler/ for CF.
@prk-Jr prk-Jr self-assigned this Apr 24, 2026
prk-Jr added 2 commits April 25, 2026 18:12
  instead of silently truncating; callers now get an explicit signal
  rather than incomplete pagination results
- Correct DEFAULT_MAX_LIST_KEYS, with_max_list_keys, and module-level
  docs to accurately describe error-return behaviour (not truncation,
  not "unbounded allocation" guarding)
- Add log::debug in SpinSecretStore::get_bytes when store_name is
  non-empty so callers learn the flat-namespace constraint at runtime
- Add comment in config_store contract tests explaining the InMemory
  backend accepts dotted/uppercase keys that the real Spin backend
  would reject via InvalidName
- Add comment in lib.rs explaining why SpinConfigStore has different
  feature gating than SpinKvStore and SpinSecretStore
Copy link
Copy Markdown
Contributor

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Thanks for adding Spin store support and the smoke/CI coverage. CI is green. I found two configuration-semantics issues worth addressing before relying on non-default store names: Spin KV dispatch is hard-coded to default, and accepting a Spin config-store adapter override is misleading because the runtime does not consume that name.

Comment thread crates/edgezero-adapter-spin/src/request.rs Outdated
Comment thread crates/edgezero-core/src/manifest.rs Outdated
prk-Jr added 2 commits May 14, 2026 12:07
Resolve conflict in test.yml: keep matrix-based wasm test job
(cloudflare/fastly/spin) from this branch; drop main's per-adapter
split jobs which pre-date Spin support.
@prk-Jr prk-Jr requested a review from ChristianPavilonis May 14, 2026 07:04
Copy link
Copy Markdown
Contributor

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed the Spin store support changes. The implementation adds useful store coverage and CI/smoke-test expansion, but a few issues need attention before merge: one wasm no-default feature build currently fails, Spin store dispatch bypasses manifest resolution, and KV TTL/listing behavior does not match the core contract.

Cross-cutting finding

  • 📝 Document Spin-specific store behavior: The user docs and secret-store rustdoc still describe KV TTL, config store behavior, and named secret stores as if Spin behaves like the other adapters. After the implementation behavior is finalized, please update docs/guide/kv.md, docs/guide/configuration.md, and crates/edgezero-core/src/secret_store.rs to cover Spin's KV limitations, component variables/flat namespace, lowercase variable names, and whether run_app honors manifest gating.

😃 Praise

  • 😃 The Spin wasm CI matrix entry plus the new smoke-test coverage are a strong addition and should catch real integration regressions across the adapter.

CI Status

  • cargo fmt --all -- --check: PASS
  • cargo clippy --workspace --all-targets --all-features -- -D warnings: PASS
  • cargo test --workspace --all-targets: PASS
  • cargo check --workspace --all-targets --features "fastly cloudflare spin": PASS
  • cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --features spin: PASS
  • Additional check: cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --no-default-features: FAIL (see inline comment on SpinConfigStore)

match &self.inner {
#[cfg(target_arch = "wasm32")]
SpinConfigBackend::Spin => {
use spin_sdk::variables;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Gate SpinConfigStore on the spin feature: spin-sdk is an optional dependency enabled only by the spin feature, but this wasm32 arm is gated only on target_arch = "wasm32". As a result, cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --no-default-features currently fails here with unresolved import spin_sdk.

Suggestion: gate the production backend consistently on both cfgs, and update the fallback branch to match:

#[cfg(all(feature = "spin", target_arch = "wasm32"))]
Spin,

#[cfg(all(feature = "spin", target_arch = "wasm32"))]
pub fn new() -> Self { ... }

#[cfg(all(feature = "spin", target_arch = "wasm32"))]
SpinConfigBackend::Spin => { ... }

/// `edgezero.toml` is set to a custom value).
pub async fn dispatch(app: &App, req: IncomingRequest) -> anyhow::Result<spin_sdk::http::Response> {
let core_request = into_core_request(req).await?;
dispatch_with_kv_label(app, req, "default").await
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Resolve Spin store handles from the manifest: run_app reaches this path, so Spin always opens the hard-coded "default" KV label and always injects config/secrets. That bypasses [stores.kv].name, [stores.kv.adapters.spin], [stores.secrets].enabled, and whether [stores.config] exists, unlike the Fastly and Cloudflare run_app(manifest_src, ...) paths. A user setting [stores.kv] name = "MY_KV" would still get no KV handle unless they manually avoid run_app and call dispatch_with_kv_label themselves.

Suggestion: add a manifest-aware Spin entry point that resolves manifest.kv_store_name(edgezero_core::app::SPIN_ADAPTER), manifest.secret_store_enabled(...), and config-store presence before dispatching, then update the Spin template to pass the embedded edgezero.toml manifest.

value: Bytes,
_ttl: Duration,
) -> Result<(), KvError> {
log::warn!("SpinKvStore: TTL is not supported by the Spin KV API; storing without expiry");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Do not report TTL writes as successful when Spin cannot expire them: put_bytes_with_ttl logs a warning but then writes the value without any expiry and returns Ok(()). The core KV contract says TTL values should be treated as expired after the TTL, so callers can accidentally retain temporary/session data indefinitely without any error signal.

Suggestion: return an explicit validation/unsupported error instead of writing when TTL cannot be honored:

return Err(KvError::Validation(
    "Spin KV does not support TTL; use put_bytes for non-expiring values".to_string(),
));

cursor: Option<&str>,
limit: usize,
) -> Result<KvPage, KvError> {
let all_keys = self
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Apply bounded-listing semantics before fetching keys: Store::get_keys() materializes the entire Spin KV store before max_list_keys is checked, so the cap does not actually prevent unbounded allocation in the request path. This also conflicts with the core KvStore::list_keys_page contract, which says implementations should keep memory bounded to a single page of keys.

Suggestion: unless Spin exposes prefix/server-side pagination, treat key listing as unsupported for this adapter rather than fetching the whole store:

Err(KvError::Validation(
    "Spin KV key listing is unsupported because Store::get_keys() is unbounded".to_string(),
))

pub name: Option<String>,
/// Per-adapter name overrides, keyed by supported lowercase adapter name
/// (`axum`, `cloudflare`, or `fastly`).
/// (`axum`, `cloudflare`, `fastly`, or `spin`).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Keep the config-store adapter docs aligned with validation: this rustdoc now says [stores.config.adapters] can be keyed by spin, but SUPPORTED_CONFIG_STORE_ADAPTERS intentionally excludes spin and the new test expects [stores.config.adapters.spin] to fail validation. That will mislead users into writing a manifest shape that EdgeZero rejects.

Suggestion: remove spin from this list and call out the reason, e.g. Spin config uses component variables in a flat namespace, so stores.config.adapters.spin is rejected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spin config/secret store implementation Spin KV store implementation

2 participants